poheng
11/28/2025, 10:40 AMMarvin
11/28/2025, 10:40 AMMarvin
11/28/2025, 10:46 AMversion: "3.9"
services:
prefect-server:
image: prefecthq/prefect:3-latest
container_name: prefect-server
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('<http://localhost:4200/api/health>', timeout=1)"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
prefect-docker-worker:
image: prefecthq/prefect:3-latest
container_name: prefect-docker-worker
depends_on:
prefect-server:
condition: service_healthy
environment:
# Point the worker to the local Prefect API
PREFECT_API_URL: <http://prefect-server:4200/api>
# Optional tuning:
# PREFECT_WORKER_PREFETCH_SECONDS: "10"
# PREFECT_WORKER_QUERY_SECONDS: "15"
volumes:
# Critical: allow the worker to create Docker containers
- /var/run/docker.sock:/var/run/docker.sock
# Use 'uv' to ensure the docker integration is available
command: >
uv run --with 'prefect[docker]'
prefect worker start
--pool docker-pool
restart: on-failure
Important notes:
- Create the Docker-type work pool once before starting the worker:
- prefect work-pool create --type docker docker-pool
- Or create it in the UI (Work Pools → Create → Type: Docker → Name: docker-pool)
- The worker needs Docker access to start flow-run containers; that’s why we mount /var/run/docker.sock.
- The uv run --with 'prefect[docker]' ensures the Docker integration is available in the worker container.
- For Prefect Cloud, remove the prefect-server service and set PREFECT_API_URL to your Cloud workspace API URL (found in your workspace settings), then keep the worker service as-is.
Handy commands:
- Bring it up: docker compose up -d
- Tail worker logs: docker compose logs -f prefect-docker-worker
- Verify workers: prefect worker ls
Docs:
- Docker deployments/workers: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Workers + work pools concepts: https://docs.prefect.io/v3/concepts/workers
- Managing work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/manage-work-pools
If you want this tailored for Prefect Cloud or with custom base job templates (e.g., private registry auth, volumes, env), tell me your details and I’ll adjust the compose for you.poheng
11/28/2025, 1:07 PMMarvin
11/28/2025, 1:07 PMMarvin
11/28/2025, 1:10 PMservices:
prefect-server:
image: prefecthq/prefect:3-latest
container_name: prefect-server
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
PREFECT_UI_ENABLED: "true"
# Explicitly tell the UI where the API lives from your browser’s perspective
PREFECT_UI_API_URL: <http://localhost:4200/api>
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
2) Verify the API from your host
- From Windows PowerShell or WSL shell:
curl -s <http://localhost:4200/api/health>
Expected: true
- If that fails, try <http://127.0.0.1:4200/api/health>
3) Check server logs
docker compose logs -f prefect-server
You should see it serving on 0.0.0.0:4200 and no errors.
4) UI symptom: “Couldn’t connect to API”
If you see that banner in the browser, it means the UI couldn’t reach /api. Setting PREFECT_UI_API_URL as above fixes the common case (browser → http://localhost:4200 → UI → http://localhost:4200/api).
5) WSL2/Windows tips
- Use http://127.0.0.1:4200 if http://localhost:4200 doesn’t work.
- Temporarily disable VPN/firewall to rule out interference.
- Make sure Docker Desktop uses the WSL2 backend (Settings → General).
- Ensure nothing else is bound to port 4200 on Windows.
If you’d like, paste:
- The compose file you’re using
- Output of:
docker compose ps
docker compose logs --tail=100 prefect-server
curl -v <http://localhost:4200/api/health>
and I’ll pinpoint the issue.
Docs for reference:
- Workers and work pools: https://docs.prefect.io/v3/concepts/workers
- Docker work pool setup: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Managing work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/manage-work-poolspoheng
11/28/2025, 3:56 PMMarvin
11/28/2025, 3:56 PMMarvin
11/28/2025, 4:00 PMjob_configuration object. Your image and env keys fit the schema.
- env must be a map of string → string/null. Using "true" is fine.
- prefecthq/prefect:3-python3.11-uv is a valid image tag.
- However, PREFECT_APPLY_MARK and PREFECT_JOB_EXTRA_COMMANDS are not recognized by Prefect/Docker worker out of the box; they’ll just be passed into your flow-run container as environment variables. They’ll only do something if your code or entrypoint reads them.
If your goal is to run uv sync --system-site-packages before executing the flow, set a custom command that runs both the sync and the flow-run execute. For example (Terraform/HCL with jsonencode):
base_job_template = jsonencode({
job_configuration = {
image = "prefecthq/prefect:3-python3.11-uv"
command = "bash -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = {
# Any env you really need at runtime; remove if not used by your code
PREFECT_APPLY_MARK = "true"
PREFECT_JOB_EXTRA_COMMANDS = "uv sync --system-site-packages"
}
}
})
If you don’t need those env vars, you can omit them entirely.
Tip: apply the template when creating/updating the pool:
prefect work-pool create my-docker-pool --type docker --base-job-template ./base-job-template.json
Docs:
- Docker work pools and job templates: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Workers overview: https://docs.prefect.io/v3/concepts/workerspoheng
11/28/2025, 4:17 PMMarvin
11/28/2025, 4:17 PMMarvin
11/28/2025, 4:20 PMgit --version
If using a Docker worker image, make sure the image includes git (the official prefect images do).
- If not installed, install git (apt-get, apk, etc., depending on your worker environment).
2) Confirm your deployment’s source settings
- If you used .from_source('<https://github.com/owner/repo.git>', entrypoint='path.py:flow'), ensure:
- URL is correct
- Branch/commit exists
- If it’s a private repo, you must provide credentials (token or SSH):
- Recommended: use a credentials block and pass it via GitRepository:
from prefect.runner.storage import GitRepository
from prefect_github import GitHubCredentials
my_flow = flow.from_source(
source=GitRepository(
url="<https://github.com/owner/repo.git>",
branch="main",
credentials=GitHubCredentials.load("my-github-creds"),
# optionally: directories=["flows"] # for sparse checkout
),
entrypoint="flows/my_flow.py:my_flow",
)
- Or embed a Secret block into credentials={"access_token": Secret.load("github-token")}.
- For SSH URLs (git@github.com:...), ensure the worker has an SSH key and host known.
3) Try the exact git clone from the worker host
- On the same machine/container where the worker runs, try:
git clone <https://github.com/owner/repo.git>
Or the exact URL (with branch/commit) your deployment uses. This helps reveal auth or network errors directly.
4) Enable debug logs to see the precise failure
- Before starting the worker:
export PREFECT_DEBUG_MODE=true
export PREFECT_LOGGING_LEVEL=DEBUG
export PREFECT_RUNNER_LOG_LEVEL=DEBUG
prefect worker start --pool <your-pool> --type process
- Then re-run; you should see a message like:
Failed to clone repository '<https://github.com/owner/repo.git>' with exit code 2.
and more context around the git command.
5) If you’re using sparse checkout or a specific commit
- Double-check directories=[...] paths exist in the repo
- Verify branch or commit_sha is valid and accessible
6) Windows/WSL specifics (if applicable)
- If the Process worker is running under WSL, ensure git is installed inside WSL and the repo URL is reachable from WSL’s network environment.
If you paste the deployment definition you used (the .from_source(...) snippet or prefect deploy details) and whether the repo is private, I can give you an exact correction. Also useful:
- Output of git --version where the worker runs
- The repo URL form (HTTPS vs SSH)
- Any custom branch/commit/directory settings you set in GitRepository
Docs:
- Deploy from source (Docker/Git/general): https://docs.prefect.io/v3/how-to-guides/deployments/
- Workers overview: https://docs.prefect.io/v3/concepts/workerspoheng
11/28/2025, 4:51 PMMarvin
11/28/2025, 4:51 PMMarvin
11/28/2025, 4:51 PMprefect worker start --pool <your-docker-pool> --type docker
- Or use the docker-compose I shared earlier
If a Process worker picks up the run, any Docker-only template fields are ignored and the command runs in the worker’s local environment, where uv may not exist, causing exit 2.
2) Your command string has HTML-escaped &&, which will break the shell
In your Terraform, I see && instead of &&. That will make sh -c ... fail with a shell syntax error and typically exit code 2.
Please change it to actual &&.
3) Variables schema: add "type": "object" or drop variables entirely
If you include a variables section, it should be a JSON Schema object. If you don’t need templating, you can omit variables completely.
Here are two corrected options.
Option A: Minimal (no variables)
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
description = ""
base_job_template = jsonencode({
job_configuration = {
image = "prefecthq/prefect:3-python3.11-uv"
# Prefer -lc so PATH and shell expansions are consistent
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
}
})
}
Option B: With variables schema (if you want to make image configurable)
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
description = ""
base_job_template = jsonencode({
job_configuration = {
image = "{{ image }}"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
}
variables = {
type = "object"
properties = {
image = {
type = "string"
default = "prefecthq/prefect:3-python3.11-uv"
title = "Docker Image"
description = ""
}
}
required = ["image"]
}
})
}
4) Double-check that uv sync is appropriate for your repo
- uv sync expects a pyproject.toml in the working directory of the flow run container.
- If your pyproject.toml lives in a subdirectory, cd before syncing:
command = "sh -lc 'cd subdir && uv sync --system-site-packages && prefect flow-run execute'"
- If you don’t need runtime sync, you can remove the uv sync step entirely.
5) Verify your deployment targets the Docker pool
- In the UI, open the deployment and confirm Work Pool = your Docker pool
- Or redeploy with the correct pool
- Ensure a Docker worker is running for that pool (not a Process worker)
If you still see exit code 2 after these changes, please share:
- The current deployment’s Work Pool name (from the UI)
- Whether a Docker worker is running and polling that pool
- The latest flow-run logs around the command start
- Whether your repo has a pyproject.toml at the root or a subdir
Docs for reference:
- Docker work pools and job templates: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Workers overview: https://docs.prefect.io/v3/concepts/workers
- Deployments from source: https://docs.prefect.io/v3/how-to-guides/deployments/poheng
11/28/2025, 5:19 PMMarvin
11/28/2025, 5:19 PMMarvin
11/28/2025, 5:24 PMterraform {
required_providers {
prefect = {
source = "PrefectHQ/prefect"
version = "~> 1.0"
}
}
}
provider "prefect" {
# Self-hosted Server
host = var.prefect_api_url # e.g., <http://localhost:4200/api> (dev) or your ingress URL
api_key = var.prefect_api_key # create a service account; for server, leave unset if auth disabled
}
Docker work pool (Git-pull friendly)
- Ensure your Docker worker container can create sibling containers: mount /var/run/docker.sock.
- In base_job_template, set a clear command that bootstraps your code env (if needed) and runs the flow.
- If you use uv/pip at runtime, ensure pyproject.toml/requirements are where the container expects; cd first if needed.
- Keep variables in the template so you can override image/env per-deployment later.
Good starting template:
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "{{ image }}"
# Prefer sh -lc so login shells and PATH behave as expected
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = {
ENVIRONMENT = var.environment
}
# Optional: map volumes or networks if your flows need them
# volumes = ["/host/path:/container/path"]
# network_mode = "bridge"
# mem_limit = "1g"
}
variables = {
type = "object"
properties = {
image = {
type = "string"
default = "prefecthq/prefect:3-python3.11-uv"
title = "Docker Image"
}
env = {
type = "object"
title = "Environment Variables"
additionalProperties = { type = ["string", "null"] }
default = {}
}
}
required = ["image"]
}
})
tags = ["environment:${var.environment}", "runtime:docker", "code-source:git"]
}
Important notes
- Use && (not HTML-escaped &&) in command strings.
- If your repo root differs, cd to subdir in the command:
- command = "sh -lc 'cd flows && uv sync --system-site-packages && prefect flow-run execute'"
- If you don’t need runtime installs, simplify to:
- command = "prefect flow-run execute"
Git source and credentials (Blocks)
- Use blocks for Git credentials to avoid leaking secrets into Terraform state.
- For GitHub: create a GitHubCredentials block with a token and reference it in deployments.
- If you must provision blocks via Terraform, store only references/indirect secrets.
Example block (safe pattern)
- Prefer creating the block at runtime (CI step) using environment secrets or prefect block create with masked input, rather than putting the token inline in Terraform.Marvin
11/28/2025, 5:24 PMresource "prefect_block" "github_credentials" {
name = "github-creds"
type_slug = "github-credentials"
data = jsonencode({
token = var.github_token # WARNING: ends up in TF state; prefer CI-time creation
})
}
Deployments: managed by CI, referenced by Terraform
- Best practice: let CI call .deploy() or prefect deploy to create/update deployments from your repo. Terraform can manage metadata around deployments if needed, but drift is common if both TF and CI manage deployments.
- If you must use Terraform’s prefect_deployment, keep it minimal and reference the Git repo + credentials block.
Deployment pattern with Git pull (preferred via code/CI)
- In your repo:
- flow = flow.from_source(source=GitRepository(...), entrypoint="path/to.py:flow_fn")
- flow.deploy(work_pool_name="your-docker-pool", name="my-deployment", parameters=..., tags=[...])
- Ensure the Docker worker is running and targets the correct pool.
Concurrency, work queues, and tagging
- Tags: apply consistent tags at pool, deployment, and automation levels (env, team, data-domain).
- Work queues: optionally create named queues per SLA or team and attach concurrency limits.
- Concurrency limits:
- Global: prefect_global_concurrency_limit
- Task-run: prefect_task_run_concurrency_limit
- Queue: set on the queue in UI or API
resource "prefect_global_concurrency_limit" "io_bound" {
name = "io-bound"
limit = 20
active = true
slot_decay_per_second = 0
}
Automations and notifications
- Set up Slack/email on failures or state changes.
- Use env tags/labels to scope automations.
resource "prefect_automation" "notify_failures" {
name = "notify-failures-${var.environment}"
trigger = {
type = "flow-run-state-change"
match = {
any_ = [
{ "flow_run": { "tags": ["environment:${var.environment}"] } }
]
}
after = "2m" # debounce
state = "Failed"
}
actions = [
{
type = "send-notification"
block_document_name = "slack-notifier"
payload = jsonencode({
message = "Flow run {{ flow_run.name }} failed in ${var.environment}"
})
}
]
}
Multi-environment layout
- Use Terraform workspaces or folders for dev/staging/prod.
- Parameterize:
- work pool names
- image tags
- API URL
- tags
- concurrency
- Keep naming conventions consistent: prefix/suffix with environment.
Workers and runtime environment
- Don’t manage worker containers with Terraform; manage them with your Docker Compose or infra orchestrator.
- In your docker-compose for the worker:
- Mount /var/run/docker.sock
- Set PREFECT_API_URL to your Server
- Optionally run via uv to include docker integration:
- command: uv run --with 'prefect[docker]' prefect worker start --pool your-docker-pool
- Ensure git is installed in the final runtime image used for flow runs if you run uv sync or other tooling.
Troubleshooting “exit code 2”
- If you see shell exit 2, common causes:
- Bad shell command (e.g., HTML-escaped &&)
- Missing tools (git, uv) in the runtime image
- Wrong working directory (pyproject.toml missing for uv)
- Validate by exec’ing into a one-off container with the same image and running your command manually:
- docker run --rm -it prefecthq/prefect:3-python3.11-uv sh -lc "git --version && uv --version && prefect --version"
Useful links
- Terraform provider registry: https://registry.terraform.io/providers/PrefectHQ/prefect/latest
- Work pools: https://docs.prefect.io/v3/concepts/work-pools
- Docker work pool how-to: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Deployments (from source): https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- Blocks: https://docs.prefect.io/v3/develop/blocks/
- Automations: https://docs.prefect.io/v3/concepts/automationsMarvin
11/28/2025, 5:24 PMpoheng
11/29/2025, 3:28 AMMarvin
11/29/2025, 3:28 AMMarvin
11/29/2025, 3:29 AMflow.from_source(..., source=GitRepository(...)), Prefect runs a git_clone “pull step” right before executing the flow. For Docker pools, that pull step runs inside the job container. If git isn’t installed in that image, the clone fails and your run crashes.
How to satisfy the requirement
- Use an image that includes git, or extend the Prefect image to add it.
Debian/Ubuntu-based:
FROM prefecthq/prefect:3-python3.11-uv
USER root
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
USER prefect
Alpine-based:
FROM prefecthq/prefect:3-python3.11-uv
USER root
RUN apk add --no-cache git
USER prefect
Then point your work pool template to this image:
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3.11-uv-with-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
}
})
}
Alternatives (no git needed at runtime)
- Bake your flow code into the image and deploy from that image (don’t use from_source/GitRepository).
- Mount your code into the container at runtime (e.g., volume) if your infra allows it.
- Use CI to build and push an image whenever code changes, then update deployments to use that image.
Extras to keep in mind
- If your repo is private, the job container also needs credentials (e.g., a GitHubCredentials block referenced by the deployment). SSH-based auth requires an SSH key and known_hosts in the container.
- If you run uv sync, ensure your pyproject.toml is present in the working directory you expect (cd to a subdir first if needed).
If you paste your current image name and how you create deployments (from_source snippet), I can suggest the minimal Dockerfile and Terraform changes tailored to your setup.poheng
11/29/2025, 4:00 AMMarvin
11/29/2025, 4:00 AMMarvin
11/29/2025, 4:02 AMresource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "{{ image }}"
# Run flow directly (no git, no uv). If you do need installs, mount a venv/cache too.
command = "sh -lc 'prefect flow-run execute'"
# Mount your host/source code into the container
volumes = ["{{ code_mount }}"]
# Optional defaults
env = "{{ env }}"
stream_output = true
}
variables = {
type = "object"
properties = {
image = {
type = "string"
default = "prefecthq/prefect:3-python3.11-uv"
title = "Docker image"
}
code_mount = {
type = "string"
title = "Code volume mount"
description = "Host:Container mount for your code and entrypoint"
# Example assumes your code lives at /opt/flows inside container
default = "/absolute/host/code:/opt/flows:ro"
}
env = {
type = "object"
title = "Environment variables"
additionalProperties = { type = ["string", "null"] }
default = {}
}
}
required = ["image", "code_mount"]
}
})
}
Key points
- The mount must be absolute on the Docker host (e.g., /home/you/project:/opt/flows).
- Use :ro for read-only if you don’t need writes.
- Ensure your deployment entrypoint matches the container path:
- If you mount /home/you/project to /opt/flows, and your flow file is flows/my_flow.py with a function my_flow, set entrypoint to /opt/flows/flows/my_flow.py:my_flow.
Creating the deployment (no from_source)
- In Prefect 3.x, you can deploy from code without Git source:
Python pattern:
from prefect import flow
@flow
def my_flow():
...
if __name__ == "__main__":
my_flow.deploy(
name="mount-code-deployment",
work_pool_name="YOUR_DOCKER_POOL",
# No image build here; rely on the pool image and the mounted code
job_variables={
"image": "prefecthq/prefect:3-python3.11-uv",
"code_mount": "/home/you/project:/opt/flows:ro",
"env": {"ENVIRONMENT": "dev"}
},
# Ensure Prefect knows where to find the entrypoint at runtime
entrypoint_type="absolute",
# Or set via prefect.yaml and `prefect deploy`
)
CLI with prefect deploy
- You can also define entrypoint in a prefect.yaml and run prefect deploy.
- Ensure the entrypoint points to the absolute path inside the container (or use a relative path with a working directory set by your command).
Worker/runtime alignment
- Your Docker worker container needs only the Docker socket; it does not need your code. The code is mounted into the flow-run container via the pool’s job template.
- In your docker-compose for the worker, keep:
- /var/run/docker.sock mounted
- PREFECT_API_URL set correctly
- Worker started with prefect worker start --pool <your-pool>
Troubleshooting tips
- Exit code 2 often comes from shell syntax or missing files. When mounting:
- Verify the host path exists and is absolute.Marvin
11/29/2025, 4:02 AMdocker run --rm -it -v /home/you/project:/opt/flows:ro prefecthq/prefect:3-python3.11-uv \
sh -lc "ls -la /opt/flows && python -c 'import runpy; runpy.run_path(\"/opt/flows/flows/my_flow.py\")'"
When you still want uv/pip
- If you need runtime installs, mount a writable location for a venv or cache:
- volumes:
- /home/you/project/opt/flowsro
- /home/you/.cache/pip:/home/prefect/.cache/pip
- command:
- "sh -lc 'cd /opt/flows && uv sync --system-site-packages && prefect flow-run execute'"
Summary
- Don’t use Git pull steps; don’t require git in runtime.
- Mount code via volumes in the Docker work pool’s base_job_template.
- Ensure deployment entrypoint matches the mounted path.
- Keep the worker simple; logic lives in the job container.
If you share your host path and the in-container path you prefer, I can tailor the exact Terraform and deployment snippets for your setup.poheng
11/29/2025, 4:07 AMMarvin
11/29/2025, 4:07 AMMarvin
11/29/2025, 4:11 AMpull:
- prefect.deployments.steps.git_clone:
id: clone-step
repository: <https://github.com/owner/repo.git>
# credentials: {{ your_github_block }} # for private repos
# branch: main
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone-step.directory }}"
script: |
uv --version
uv sync --system-site-packages
Entrypoint resolution
- Prefect resolves the entrypoint relative to the current working directory (the cloned repo). So:
- entrypoint="flows/my_flow.py:my_flow" works if flows/ is under repo root
- If your flow is deeper, point relative to root or use an absolute path (not usually necessary).
Putting it together with Terraform
- Your work pool’s base_job_template command can reference uv directly, no need to set a custom working directory — Prefect sets cwd to the clone directory.
- Ensure your image contains git and uv if you use uv (git required for private deps and submodules).
Example base_job_template (Terraform) for Git-based deployments:
```
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "{{ image }}"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = "{{ env }}"
# Optional: networks/volumes/limits as needed
}
variables = {
type = "object"
properties = {
image = {
type = "string"
default = "prefecthq/prefect:3-python3.11-uv"
}
env = {
type = "object"
additionalProperties = { type = ["string", "null"] }Marvin
11/29/2025, 4:11 AMpoheng
11/29/2025, 4:35 AMMarvin
11/29/2025, 4:35 AMMarvin
11/29/2025, 4:35 AMdocker run --rm prefecthq/prefect:3-python3.11-uv sh -lc "git --version || echo 'git not found'"
If you see “git not found”, you need a custom image.
Best practice: build a thin custom image with git
- Debian/Ubuntu-based:
FROM prefecthq/prefect:3-python3.11-uv
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client \
&& rm -rf /var/lib/apt/lists/*
USER prefect
- If you use SSH for Git, also add known_hosts:
RUN mkdir -p /home/prefect/.ssh \
&& ssh-keyscan -t rsa <http://github.com|github.com> >> /home/prefect/.ssh/known_hosts \
&& chown -R prefect:prefect /home/prefect/.ssh
Point your work pool at this image in Terraform
base_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3.11-uv-with-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
}
variables = {
type = "object"
properties = {
image = { type = "string", default = "your-registry/prefect:3.11-uv-with-git" }
}
required = ["image"]
}
})
Can I install git at runtime instead?
- You could, but it’s slow and often won’t work because the container runs as a non-root user (prefect). e.g., apt-get would fail without root. Building once into your image is the robust approach.
Why git is required here
- You’re using Git-based deployments (flow.from_source(..., source=GitRepository(...))). Prefect runs a git_clone pull step inside the flow-run container before executing your flow. If git isn’t available there, the run crashes (often exit code 2).
If you want, share your current image name and I’ll suggest the exact Dockerfile plus the minimal Terraform change to reference it.
Docs:
- Docker work pool: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Create deployments (from source): https://docs.prefect.io/v3/how-to-guides/deployments/create-deploymentspoheng
11/29/2025, 5:13 AMMarvin
11/29/2025, 5:13 AMMarvin
11/29/2025, 5:13 AM# runtime image used by the Docker work pool for flow runs
FROM prefecthq/prefect:3-python3.11-uv
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Optional: add GitHub host key for SSH-based clones
# RUN mkdir -p /home/prefect/.ssh \
# && ssh-keyscan -t rsa <http://github.com|github.com> >> /home/prefect/.ssh/known_hosts \
# && chown -R prefect:prefect /home/prefect/.ssh
USER prefect
- Alpine base (if you prefer)
FROM prefecthq/prefect:3-python3.11-uv
USER root
RUN apk add --no-cache git openssh-client ca-certificates
USER prefect
2) Push the image to your registry and point your Docker work pool to it (Terraform)
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3.11-uv-with-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
# add env/volumes/etc as needed
}
})
}
3) Make sure your deployment targets this Docker work pool (not Process), and that a Docker worker is running for it:
- Start worker:
uv run --with 'prefect[docker]' prefect worker start --pool <your-docker-pool> --type docker
- In the UI, verify the deployment’s Work Pool = your Docker pool. Your flow-run logs should show “DockerWorker …” not “ProcessWorker …”.
4) Quick verification
docker run --rm your-registry/prefect:3.11-uv-with-git sh -lc "git --version && uv --version && prefect --version"
Only install git in the worker container if you’re using a Process work pool (since the worker is the runtime in that case). For a Docker work pool, keep the worker minimal; just ensure it has Docker access (/var/run/docker.sock) and can reach the API.
Docs for reference:
- Docker work pool how-to: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Deployments from source (Git): https://docs.prefect.io/v3/how-to-guides/deployments/create-deploymentspoheng
11/29/2025, 7:43 AMMarvin
11/29/2025, 7:43 AMMarvin
11/29/2025, 7:45 AMdocker run --rm -it <your-image> sh -lc "git --version || echo 'git not found'; uv --version || echo 'uv not found'; prefect --version"
Recommended custom runtime image (Debian/Ubuntu-based)
FROM prefecthq/prefect:3-latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Optional: if you want uv in the image
RUN curl -LsSf <https://astral.sh/uv/install.sh> | sh -s -- -y \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
# Optional: add known_hosts for SSH-based clones
# RUN mkdir -p /home/prefect/.ssh \
# && ssh-keyscan -t rsa <http://github.com|github.com> >> /home/prefect/.ssh/known_hosts \
# && chown -R prefect:prefect /home/prefect/.ssh
USER prefect
Alpine-based alternative
FROM prefecthq/prefect:3-latest
USER root
RUN apk add --no-cache git openssh-client ca-certificates curl
RUN curl -LsSf <https://astral.sh/uv/install.sh> | sh -s -- -y \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
USER prefect
Point your Docker work pool to this image (Terraform)
resource "prefect_work_pool" "docker_pool" {
name = var.work_pool_name
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3-with-uv-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = {}
}
})
}
Notes
- Git is required in the job image (the flow-run container) for Git-based deployments because Prefect clones your repo inside that container at run time.
- The worker container (in your docker-compose) does not need git; it just submits jobs to Docker.
- If you use SSH for private repos, include openssh-client and known_hosts (or a credentials block for HTTPS tokens).
- Pin the Prefect base tag (e.g., 3.x.y) for repeatability.
Docs
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Create deployments from source (Git): https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
If you share the exact tag you’re using now, I can confirm what’s inside and tailor the Dockerfile accordingly.poheng
11/29/2025, 8:40 AMMarvin
11/29/2025, 8:40 AMMarvin
11/29/2025, 8:40 AMFROM prefecthq/prefect:3.9.x-python3.11-slim # pin an exact 3.x version
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install uv once (pin if you prefer)
RUN curl -LsSf <https://astral.sh/uv/install.sh> | sh -s -- -y \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
# Copy dependency manifests first to cache installs
WORKDIR /opt/app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --system-site-packages
# If your dbt project lives in repo, you may install adapters here instead of pyproject
# RUN uv pip install dbt-core dbt-postgres # if not in pyproject
# Drop to non-root
USER prefect
- Build per repo/project (one image per dbt project or per team). Tag images with app version or git SHA.
3) Production deployments (preferred: image-based)
- Don’t do runtime installs. Your base_job_template command can be:
- prefect flow-run execute
- Entrypoint points at code inside the image (COPY code in the Docker build) or at a mounted common code dir if you standardize that.
- In Terraform, your docker pool template’s image defaults to the prebuilt tag. Keep env for secrets/flags (non-sensitive).
4) Fast-iteration deployments (optional: Git-based)
- Keep a “dev” or “preview” work pool that uses an image with git + uv and does:
- sh -lc 'uv sync --system-site-packages && prefect flow-run execute'
- Deployment created with flow.from_source(source=GitRepository(...), entrypoint=...)
- Useful for POCs or quick tests; slower cold starts; requires outbound access to your Git host and package index.
5) dbt execution pattern
- Use the Prefect dbt integration (DbtCoreOperation) or a simple task/command wrapper.
- Keep dbt profiles in a Prefect block or mounted secret file; never store credentials in code or TF state.
- Example task:
from prefect import flow
from prefect_dbt import DbtCoreOperation
@flow
def run_dbt(models: list[str] = None):
DbtCoreOperation(
commands=["dbt deps", "dbt build --select " + " ".join(models or ["state:modified+"])],
env={"DBT_PROFILES_DIR": "/opt/app/profiles"}, # mount or baked path
).run()
6) Secrets, credentials, and compliance
- Never commit secrets; do not put raw secrets in Terraform state.
- Use Prefect Blocks for:
- Warehouse creds (e.g., connection URL/creds)Marvin
11/29/2025, 8:40 AMprefect deploy) to avoid drift between TF and code.
9) Migration to Kubernetes (later)
- Swap the Docker work pool for a Kubernetes work pool using a job template.
- Reuse the same runtime images and most deployment settings.
- Mount ConfigMaps/Secrets for dbt profiles; use k8s service account and network policies per hospital rules.
Choosing between Git-based vs image-based (rules of thumb)
- Prefer image-based for production dbt pipelines:
- Reproducible, fast startup, no internet at runtime, easier change control and scanning
- Use Git-based for quick iteration/preview:
- Faster to change, but slower cold start and needs git + internet in the job container
- If your hospital network restricts outbound internet from runtime nodes, image-based is almost required.
Operational checklist
- One docker-compose stack for Prefect Server per env.
- 1–2 Docker workers per env; watch their logs and set restart policies.
- Private registry for your runtime images; CI builds on merge to main; tag immutably.
- Pin Prefect image and Python versions; pin dbt adapters and dependencies in uv.lock.
- Health checks and Slack alerts wired from Automations.
Useful links
- Work pools: https://docs.prefect.io/v3/concepts/work-pools
- Docker work pool how-to: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Deployments (create, from source): https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- dbt integration: https://docs.prefect.io/v3/integrations/collection/prefect-dbt/
- Automations: https://docs.prefect.io/v3/concepts/automations
- Terraform provider: https://registry.terraform.io/providers/PrefectHQ/prefect/latest
If you share:
- Your target warehouse (and dbt adapter)
- Whether runtime has outbound internet
- Your preferred repo layout
I can sketch a minimal Dockerfile and the exact Terraform base_job_template for both prod (image-based) and dev (git-based) that your team can adopt immediately.poheng
11/29/2025, 1:15 PMMarvin
11/29/2025, 1:15 PMMarvin
11/29/2025, 1:16 PMFROM prefecthq/prefect:3-latest
USER root
# Install git + SSH client (for private repos) + certs + curl
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client curl \
&& rm -rf /var/lib/apt/lists/*
# Install uv
RUN curl -LsSf <https://astral.sh/uv/install.sh> | sh -s -- -y \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
# Optional: add GitHub known_hosts if you use SSH URLs
# RUN mkdir -p /home/prefect/.ssh \
# && ssh-keyscan -t rsa <http://github.com|github.com> >> /home/prefect/.ssh/known_hosts \
# && chown -R prefect:prefect /home/prefect/.ssh
USER prefect
2) pyproject.toml (minimal; uv will read this in the cloned repo)
[project]
name = "half-baked-demo"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
3) flows/flow.py (your demo flow)
from prefect import flow, get_run_logger
@flow
def hello(name: str = "world"):
logger = get_run_logger()
<http://logger.info|logger.info>("Hello, %s!", name)
if __name__ == "__main__":
hello()
4) prefect.yaml (deployment spec that clones the repo and syncs deps with uv)
- Replace https://github.com/your-org/your-repo.git with your repo URL (public or private).
- If private, add a credentials block later (I can help wire that up).
name: half-baked-demo
deployments:
- name: hello
entrypoint: flows/flow.py:hello
work_pool:
name: docker-pool
# Tell the job to use the image you build locally below
job_variables:
image: half-baked:latest
# Pull steps: clone repo then run uv
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your-org/your-repo.git>
branch: main
# For private repos, add a credentials block here; ask me for a snippet.
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
uv --version
uv sync --system-site-packages
5) docker-compose.yaml (self-hosted server + Docker worker)
version: "3.9"
services:
prefect-server:
image: prefecthq/prefect:3-latest
container_name: prefect-server
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('<http://localhost:4200/api/health>', timeout=1)"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
prefect-docker-worker:
image: prefecthq/prefect:3-latest
container_name: prefect-docker-worker
depends_on:
prefect-server:
condition: service_healthy
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Ensure docker integration is available inside worker
command: >
uv run --with 'prefect[docker]'
prefect worker start
--pool docker-pool
restart: on-failure
How to run it
1) Build your runtime image (contains git + uv)
docker build -t half-baked:latest .
2) Start Prefect Server and the Docker worker
docker compose up -d
UI will be at http://localhost:4200 in your browser (WSL2 tip: use 127.0.0.1 if localhost is finicky).
3) Create the Docker work pool (once)
prefect work-pool create docker-pool --type docker
4) Deploy from your repoMarvin
11/29/2025, 1:16 PMprefect deploy -n hello
5) Kick off a run
- In the UI, find the “hello” deployment and click Run.
- Or CLI:
prefect deployment run 'half-baked-demo/hello' --param name=WSL2
You should see the run logs showing:
- git clone step
- uv sync
- flow execution (“Hello, WSL2!”)
Private repo (optional)
- Create a GitHubCredentials block and reference it in the git_clone step. If you want, I’ll add that block wiring and a secure pattern to avoid storing tokens in Terraform state.
Common WSL2 tips
- If UI doesn’t load, verify: curl http://localhost:4200/api/health returns true
- Ensure nothing else is bound to port 4200
- Use 127.0.0.1:4200 if localhost is weird with VPN/firewall
- Worker logs: docker compose logs -f prefect-docker-worker
If you share your repo URL (or whether it’s private), I can add the credentials block snippet and adjust the prefect.yaml accordingly.poheng
11/29/2025, 2:44 PMMarvin
11/29/2025, 2:44 PMMarvin
11/29/2025, 2:46 PMprefect deploy
The key rule: avoid double-managing the same resource. Let Terraform own infra; let prefect.yaml/CI own deployments.
How they work together (recommended workflow)
1) Provision infra with Terraform
- Create a Docker work pool with a variable-driven base_job_template
- Set global/queue concurrency limits
- Add automations (Slack/email on failure)
- Create non-secret blocks (e.g., Slack webhook); handle secrets outside TF if possible
2) Define deployments in your repo’s prefect.yaml
- Reference the Terraform-created work pool by name
- Provide job_variables that match the pool’s base_job_template variables (e.g., image/env)
- Add pull steps for Git-based runs (git_clone, uv sync) if you use the “half-baked” approach
3) CI/CD runs prefect deploy
- On merge to main, CI reads prefect.yaml and upserts deployments
- No Terraform changes needed for app-level iteration
- Workers pick up the new/updated deployments automatically
4) Run workers separately
- Keep your Docker worker(s) running via docker-compose (not Terraform)
- Workers are “stateless processes” that poll work pools; treat them like runtime services, not drift-managed infra
Contract between Terraform work pool and prefect.yaml deployments
- Terraform defines the base_job_template with a variables schema—this is the “contract” your deployments must satisfy via job_variables.
- prefect.yaml passes values for those variables per deployment (e.g., which image to run, extra env).
Example: Terraform work pool (Docker) with variables
resource "prefect_work_pool" "docker_pool" {
name = "docker-dev"
type = "docker"
base_job_template = jsonencode({
job_configuration = {
image = "{{ image }}"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = "{{ env }}"
# optional: volumes, networks, resources
}
variables = {
type = "object"
properties = {
image = { type = "string", default = "your-registry/prefect:3.11-uv-git" }
env = {
type = "object"
additionalProperties = { type = ["string", "null"] }
default = {}
}
}
required = ["image"]
}
})
tags = ["environment:dev", "runtime:docker", "code-source:git"]
}
Example: prefect.yaml that plugs into the pool
name: my-project
deployments:
- name: hello
entrypoint: flows/flow.py:hello
work_pool:
name: docker-dev
job_variables:
image: your-registry/prefect:3.11-uv-git
env:
ENVIRONMENT: dev
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your-org/your-repo.git>
branch: main
# credentials: {{ your_github_block }} # for private repos
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
uv --version
uv sync --system-site-packages
Deployment flow
- Terraform apply (once) → creates work pool docker-dev
- CI job (on merge) runs:
- prefect deploy -n hello
- The deployment references docker-dev and supplies job_variables fitting the template
- Docker worker is already running and polls docker-dev
Secrets handling
- Do not place raw secrets in Terraform state if you can avoid it.
- Preferred:Marvin
11/29/2025, 2:46 PMprefect_deployment for those
- If you choose Terraform to manage deployments, do not run prefect deploy for those names
Multi-environment pattern
- Terraform workspaces or separate stacks per env (dev/stage/prod) to create pools, automations, limits with environment tags/names
- In repo, either:
- Have multiple deployments in prefect.yaml pointing to different pools, or
- Parameterize pool names via CI env and pass flags to prefect deploy as needed
Migration path to Kubernetes
- Swap the Terraform-managed pool from Docker to Kubernetes later
- Keep the same application-level prefect.yaml; only adjust job_variables to match the new pool’s template
Useful links
- Terraform provider: https://registry.terraform.io/providers/PrefectHQ/prefect/latest
- Work pools: https://docs.prefect.io/v3/concepts/work-pools
- Docker work pool how-to: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Deployments with prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
If you share your current TF work pool template and a sample prefect.yaml, I can sanity-check the contract between variables and job_variables so they work seamlessly.poheng
11/29/2025, 6:17 PMMarvin
11/29/2025, 6:18 PMMarvin
11/29/2025, 6:19 PMbase_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3.11-uv-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
env = {
PREFECT_API_URL = "<http://host.docker.internal:4200/api>"
}
}
})
Notes:
- Docker Desktop supports host.docker.internal inside Linux containers.
- If that doesn’t work in your setup, hardcode your Windows host IP instead.
Option B: Put job containers on the same Docker network as the server
- Then you can keep PREFECT_API_URL = http://prefect-server:4200/api and rely on service discovery
Compose with a named network:
version: "3.9"
networks:
prefect:
name: prefect-network
driver: bridge
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
ports:
- "4200:4200"
networks: ["prefect"]
prefect-docker-worker:
image: prefecthq/prefect:3-latest
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
uv run --with 'prefect[docker]'
prefect worker start
--pool docker-pool
networks: ["prefect"]
Then tell the Docker work pool to attach job containers to that same network:
base_job_template = jsonencode({
job_configuration = {
image = "your-registry/prefect:3.11-uv-git"
command = "sh -lc 'uv sync --system-site-packages && prefect flow-run execute'"
networks = ["prefect-network"]
}
})
Find which hostname failed
- In Prefect UI open the flow run → Details → Infrastructure logs. You’ll usually see which host failed to resolve (e.g., prefect-server, github.com, pypi.org).
- Or grab the job container logs:
- docker ps -a to find the container
- docker logs <container_id>
If the failure was github.com or pypi.org:
- Your job container needs outbound DNS/internet. In hospital networks, you may need a proxy:
- Add env to the job template:
env = {
HTTP_PROXY = "<http://proxy:port>"
HTTPS_PROXY = "<http://proxy:port>"
NO_PROXY = "localhost,127.0.0.1,.yourdomain"
}
- Also ensure git and uv are in your job image (you’re using Git-based deploys).
Quick tests
- API reachable via network name:
docker run --rm --network prefect-network your-registry/prefect:3.11-uv-git \
sh -lc "curl -sSf <http://prefect-server:4200/api/health> && echo OK"
- API reachable via host.docker.internal:
docker run --rm your-registry/prefect:3.11-uv-git \
sh -lc "curl -sSf <http://host.docker.internal:4200/api/health> && echo OK"
Most teams on WSL2 choose Option A (host.docker.internal) because it avoids managing Docker networks across compose and worker-launched containers. If you want, share your current base_job_template and compose, and I’ll patch them for your exact setup.poheng
11/29/2025, 6:32 PMMarvin
11/29/2025, 6:32 PMMarvin
11/29/2025, 6:33 PMdeployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
Notes:
- Docker Desktop supports host.docker.internal inside Linux containers.
- If it doesn’t resolve in your setup, replace with your host IP (or 127.0.0.1 if it works).
Option B: Put job containers on the same Docker network as the server
- Define a named network in your docker-compose and attach both services.
- Then tell the job to join that same network so PREFECT_API_URL can stay http://prefect-server:4200/api.
Compose example (add a named network):
networks:
prefect:
name: prefect-network
driver: bridge
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
ports:
- "4200:4200"
networks: ["prefect"]
prefect-docker-worker:
image: prefecthq/prefect:3-latest
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
uv run --with 'prefect[docker]'
prefect worker start
--pool docker-pool
networks: ["prefect"]
Then in prefect.yaml, add the network to job_variables so the flow-run container lands on that same network:
deployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Which should you choose?
- Most WSL2 laptops: Option A (host.docker.internal) is quickest.
- If you prefer service discovery by name and a closed network: Option B.
Quick validation
- After updating prefect.yaml, redeploy:
prefect deploy -n your-deployment
- Trigger a run and check logs. If you still see socket.gaierror, the failing hostname is in the logs (e.g., prefect-server, github.com, pypi.org). If it’s Git/PyPI, you may need proxy env vars in job_variables.env:
HTTP_PROXY, HTTPS_PROXY, NO_PROXY
Docs
- Deployments via prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
If you paste your current prefect.yaml (redact secrets), I can patch it to the exact working version for your WSL2 setup.poheng
11/29/2025, 7:02 PMMarvin
11/29/2025, 7:02 PMMarvin
11/29/2025, 7:03 PMdeployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:3.11-with-uv-git # ensure uv + git are in this image
# Optional: if you’re on WSL2 and using host.docker.internal for API
# env:
# PREFECT_API_URL: <http://host.docker.internal:4200/api>
command: "sh -lc 'uv --version || python -m pip install -U uv; uv sync --system-site-packages && prefect flow-run execute'"
Notes:
- Ensure pandas is listed in your repo’s pyproject.toml (and ideally uv.lock).
- If pyproject.toml lives in a subdir, prepend a cd:
- command: "sh -lc 'cd app && uv sync --system-site-packages && cd - && prefect flow-run execute'"
Option B: Install deps via a pull step (more explicit)
- Keep the default command and add a run_shell_script step after git_clone:
deployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:3.11-with-uv-git
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your-org/your-repo.git>
branch: main
# credentials: {{ your_github_block }} # if private
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
uv --version || python -m pip install -U uv
uv sync --system-site-packages
Prereqs checklist
- Your job image must include:
- git (for cloning)
- uv (since you’re calling it), or install uv on the fly as shown
- pandas must be declared in pyproject.toml (or requirements.txt if you use pip)
- Align Python versions between your local dev and the job image (your logs show Python 3.12; if you built for 3.11, use a 3.11 image/tag to match your lockfile)
Quick verification
- Test the job image locally:
docker run --rm -it your-registry/prefect:3.11-with-uv-git \
sh -lc "git --version; uv --version; python -c 'import sys; print(sys.version)'"
- After updating prefect.yaml, redeploy:
prefect deploy -n your-deployment
If you paste your current prefect.yaml and where pyproject.toml lives (repo root or subdir), I can tailor the exact command/pull-step for your layout. Helpful docs:
- Deployments via prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/poheng
11/29/2025, 7:31 PMMarvin
11/29/2025, 7:31 PMMarvin
11/29/2025, 7:32 PMFROM prefecthq/prefect:3-latest
# Optional: pin exact Prefect and Python variants for reproducibility
# FROM prefecthq/prefect:3.9.x-python3.11
WORKDIR /opt/prefect
# Install system packages you might need for dbt/adapters (example)
# USER root
# RUN apt-get update && apt-get install -y --no-install-recommends \
# build-essential ca-certificates git openssh-client \
# && rm -rf /var/lib/apt/lists/*
# USER prefect
# Copy dependency manifests first for better Docker layer caching
COPY pyproject.toml ./
# If you use a lock file, copy it too (poetry.lock/uv.lock/etc.)
# COPY uv.lock ./
# Install your Python deps (pip reads PEP 621/pyproject)
# If using poetry/uv, I can switch this to `uv sync` for you
RUN python -m pip install --upgrade pip \
&& pip install .
# Now copy your application code
COPY flows ./flows
# Default command: Prefect will load/execute the flow via entrypoint
# (no git or runtime installs needed)
2) pyproject.toml (declare deps like pandas)
[project]
name = "baked-demo"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"pandas>=2.2.0",
# "dbt-core==<pin>", "dbt-<your-adapter>==<pin>" # if you use dbt
]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
3) flows/flow.py (simple flow)
from prefect import flow, get_run_logger
import pandas as pd # baked into the image
@flow
def hello(name: str = "world"):
logger = get_run_logger()
<http://logger.info|logger.info>("Pandas version: %s", pd.__version__)
<http://logger.info|logger.info>("Hello, %s!", name)
if __name__ == "__main__":
hello()
4) prefect.yaml (deployment that uses your baked image)
- No pull steps — code is already in the image
- For WSL2 + docker-compose server, set PREFECT_API_URL so jobs can reach the API
- Option A: host.docker.internal (simplest)
- Option B: put jobs on same Docker network as your server and use http://prefect-server:4200/api
name: baked-demo
deployments:
- name: hello
# Use an absolute path inside the container for reliability
entrypoint: /opt/prefect/flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
image: your-dockerhub-or-registry/baked-demo:latest
# WSL2 Option A: talk to the host-mapped Prefect API port
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# If you prefer Option B (same Docker network), instead include:
# networks:
# - prefect-network
# env:
# PREFECT_API_URL: <http://prefect-server:4200/api>
5) Build and run
- Build image locally
docker build -t your-dockerhub-or-registry/baked-demo:latest .
- Start/verify Prefect Server + Docker worker (your existing docker-compose). For WSL2, ensure:
- Server publishes 4200:4200 and listens on 0.0.0.0
- Worker is running and polling your docker-pool
- Create the Docker work pool once (if not already)
prefect work-pool create docker-pool --type docker
- Deploy using prefect.yaml
prefect deploy -n hello
- Trigger a run
prefect deployment run "baked-demo/hello" --param name=WSL2
WSL2 networking note
- If you see socket.gaierror or “Cannot connect to API,” switch to the named network approach:
- In docker-compose, add a network (prefect-network) and attach both prefect-server and worker to itMarvin
11/29/2025, 7:32 PMpoheng
11/29/2025, 9:04 PMMarvin
11/29/2025, 9:04 PMMarvin
11/29/2025, 9:05 PMuv sync at build time in /opt/prefect, which by default creates a virtualenv (.venv) there. Prefect does not activate that venv when it imports your flow, so pandas isn’t on sys.path → ModuleNotFoundError.
Fix it with one of these approaches:
Option A (recommended for Git-based runs): install deps at runtime after git_clone
- Either set the job command to run uv sync before executing the flow:
# prefect.yaml
deployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:with-uv-git
command: "sh -lc 'uv --version || python -m pip install -U uv; uv sync --system-site-packages && prefect flow-run execute'"
- Or add a pull step after git_clone:
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your/repo.git>
branch: main
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
uv --version || python -m pip install -U uv
uv sync --system-site-packages
Why this works: Prefect sets cwd to the cloned repo. Running uv sync --system-site-packages there installs pandas (and all deps from that repo’s pyproject.toml) into the container’s system site-packages, so the subsequent import succeeds.
Option B (bake deps into system site-packages at build time; then drop git_clone)
- If you want to keep baking, don’t rely on a venv created at build. Install into system site-packages:
# Dockerfile
FROM prefecthq/prefect:3-latest
WORKDIR /opt/prefect
# ensure uv is present; skip if your base image already has uv
# RUN curl -LsSf <https://astral.sh/uv/install.sh> | sh -s -- -y && ln -s /root/.local/bin/uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --system-site-packages
COPY flows ./flows
- Then remove the git_clone pull step from your deployment and just use the baked image. This is true “baked” mode.
Other important notes
- Keep Python versions aligned: your logs show Python 3.12. If your uv.lock was generated for 3.11, resolve with the same Python (use a 3.11 image tag) or regenerate the lock for 3.12.
- If pyproject.toml lives in a subdirectory, cd before syncing:
- command: sh -lc 'cd app && uv sync --system-site-packages && cd - && prefect flow-run execute'
- Ensure your job image has uv and git if you’re doing Git-based runs and uv sync at runtime.
Quick sanity check
- After you update, redeploy:
prefect deploy -n your-deployment
- Trigger a run; you should see uv sync in logs before the flow import, and pandas will import.
If you paste your current prefect.yaml (redact secrets), I’ll patch it to the exact working version for your layout (repo path of pyproject, Python version, and whether you want Option A or B).poheng
11/29/2025, 9:38 PMMarvin
11/29/2025, 9:38 PMMarvin
11/29/2025, 9:39 PMuv sync --system-site-packages
but your uv version doesn’t support that flag. That option belongs to venv creation, not sync. Fix it by either installing into system site-packages with uv pip (recommended) or by using a venv and activating it at runtime.
Pick one:
Option 1 (recommended): install into system site-packages at build time
- Works for both baked-image and git-pull runs (pandas available globally)
# Dockerfile
FROM prefecthq/prefect:3.6.5-python3.11 # pin to match your lock; or regenerate lock for 3.12 if you prefer
WORKDIR /opt/prefect
# Ensure uv is available
RUN python -m pip install -U pip uv
# Copy manifests first for layer caching
COPY pyproject.toml uv.lock ./
# Install project + deps into system site-packages (uses lock)
RUN uv pip install --system --frozen .
# If you’re doing baked-image (no git pull), copy code:
# COPY flows ./flows
Notes:
- If you stay on prefecthq/prefect:3-latest (often Python 3.12), regenerate uv.lock for 3.12 or switch to a 3.11-tagged image to match your existing lock.
- After this, pandas is importable without activating a venv.
Option 2: use a venv and activate it at runtime (for git-pull)
- If you prefer uv’s venv, keep sync (no system install), but activate the venv when the container runs the flow.
Dockerfile:
FROM prefecthq/prefect:3.6.5-python3.11
WORKDIR /opt/prefect
RUN python -m pip install -U pip uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen # creates .venv here
In prefect.yaml deployment, set the job command so the venv is used:
deployments:
- name: your-deployment
entrypoint: flows/flow.py:your_flow
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:with-uv
command: "sh -lc '. .venv/bin/activate && prefect flow-run execute'"
Option 3: just use pip at build time (simple and robust)
FROM prefecthq/prefect:3.6.5-python3.11
WORKDIR /opt/prefect
COPY pyproject.toml ./
RUN python -m pip install -U pip && pip install .
# COPY flows ./flows # only for baked-image mode
Important alignment
- Python version vs lock file: if your lock was created for 3.11, use a Python 3.11-based Prefect image (e.g., 3.x-python3.11) or regenerate the lock for 3.12.
- Baked-image vs git-pull:
- Baked-image: remove git_clone pull steps and set entrypoint to the path inside your image (e.g., /opt/prefect/flows/flow.py:hello).
- Git-pull: keep git_clone; Option 1 or 2 ensures pandas is available even when Prefect imports the cloned file.
After updating
- Rebuild: docker build -t your-registry/prefect:with-deps .
- Redeploy: prefect deploy -n <your-deployment>
- Run a test: trigger a run and confirm no ModuleNotFoundError
Docs
- Deployments via prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
If you share which route you’re choosing (system install vs venv) and your current prefect.yaml, I’ll tailor the exact snippets for your setup.poheng
11/30/2025, 7:07 AMMarvin
11/30/2025, 7:07 AMMarvin
11/30/2025, 7:08 AMprefect block ls
- Inspect a block:
prefect block inspect <block-type-slug>/<block-name>
- Delete a block:
prefect block delete <block-type-slug>/<block-name>
Two minimal prefect.yaml variants for clarity
1) Git-based (keep git_clone + install deps at runtime)
name: my-project
deployments:
- name: hello
entrypoint: flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:with-uv-git
# For WSL2 to reach local server:
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# Optional: networks: ["prefect-network"] if you use a named network
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your-org/your-repo.git>
branch: main
# credentials: github-credentials/my-creds # only if private
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
uv --version || python -m pip install -U uv
uv sync && echo "deps ready"
2) Baked-image (no git, no runtime installs)
name: my-project
deployments:
- name: hello
entrypoint: /opt/prefect/flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect:baked-with-deps
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# No pull/build steps
Key points
- Don’t double-manage: if you use the baked image path, remove git_clone and any credentials references from prefect.yaml. If you use docker-git, ensure the job image has git + uv and install deps in the cloned repo.
- Deleting blocks is optional. Just make sure you’re not deleting a block that a deployment still references; otherwise runs will fail when the step tries to load it.
Docs
- Blocks overview: https://docs.prefect.io/v3/develop/blocks/
- Deployments via prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deployments
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
If you paste your current prefect.yaml, I can mark exactly which lines to keep/remove for the mode you want.poheng
11/30/2025, 3:24 PMMarvin
11/30/2025, 3:24 PMMarvin
11/30/2025, 3:24 PMname: my-project
deployments:
- name: hello-git
entrypoint: flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
# Base image the Docker work pool will use for the job container
image: prefecthq/prefect:3-latest
# WSL2 tip: point the job at your local server's API
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# Pull steps: clone the repo then install deps in the cloned directory
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://github.com/your-org/your-repo.git>
branch: main
# For private repos, add:
# credentials: github-credentials/your-block-name
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
python -m pip install -U uv
uv --version
uv sync # installs deps from pyproject.toml (and uv.lock if present)
B) Baked-into-image (no git at runtime)
- Your image already contains your code and dependencies
- Fast, reproducible, and works in restricted networks
name: my-project
deployments:
- name: hello-baked
# Point to your flow inside the image (absolute path is most reliable)
entrypoint: /opt/prefect/flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
# Your prebuilt image that already has code + deps baked in
image: your-registry/my-project:latest
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# No pull/build steps needed for baked image
What’s different between the two
- Git-based:
- Has pull steps (git_clone + run_shell_script)
- Uses a generic base image; installs deps at runtime in the cloned repo
- entrypoint is relative to the repo root (flows/flow.py:hello)
- Baked image:
- No pull steps
- Uses your prebuilt image with code + deps included
- entrypoint points to the path inside your image (e.g., /opt/prefect/flows/flow.py:hello)
Tips
- If you want the Git-based job to use a custom image that already includes git and uv, set job_variables.image to that image; the pull steps still run, but uv will be available without pip installing it.
- If you prefer networking by Docker service name instead of host.docker.internal, put your server and worker on a named network in docker-compose and set:
- env.PREFECT_API_URL: http://prefect-server:4200/api
- job_variables.networks: ["prefect-network"]
How to deploy
- From your repo root:
- Git-based: prefect deploy -n hello-git
- Baked image: prefect deploy -n hello-baked
If you share your repo URL structure (where pyproject.toml and flows live) or your image tag, I can tailor these exactly (e.g., add a cd subdir for uv sync or set an absolute entrypoint path).poheng
11/30/2025, 3:31 PMMarvin
11/30/2025, 3:31 PMMarvin
11/30/2025, 3:32 PM. .venv/bin/activate && ... — that only works if your image contains a .venv at runtime. If you didn’t create one during build, this will fail. For git-pull, a safer command is to run uv (or pip) in the cloned repo before execute.
- In Slack the && appears as `&&`; be sure your file has real &&.
Two ready-to-use variants based on your file
A) Git-based code pull (docker-git style)
- Keep pull; optionally keep build/push if you want a custom base image
- Install deps in the cloned repo before import
name: prefect_demo
prefect-version: 3.6.4
# Optional: keep these if you want a custom base image for the git-pull run
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# Required for git-pull
pull:
- prefect.deployments.steps.git_clone:
repository: <https://github.com/pohengchou/prefect_demo.git>
branch: main
# For private repos, prefer a block:
# credentials: github-credentials/github-pat-token
# Deployments
deployments:
- name: my_first_deployment
version: "1.0"
tags: [demo, docker]
description: "我的第一個deployment"
schedule:
cron: "*/2 * * * *"
timezone: "Asia/Taipei"
flow_name: main
entrypoint: flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
# If you kept build/push, use the built image; otherwise point at any image that has git + uv
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
env:
# WSL2: point jobs at your local server
PREFECT_API_URL: <http://host.docker.internal:4200/api>
# Install deps in the cloned repo, then execute
command: "sh -lc 'python -m pip install -U uv && uv sync && prefect flow-run execute'"
B) Baked-into-image (no git at runtime)
- Keep build/push; remove pull
- Ensure your Dockerfile installs your deps into the image (pip or uv system install)
```
name: prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked image
deployments:Marvin
11/30/2025, 3:32 PMpoheng
11/30/2025, 3:33 PMMarvin
11/30/2025, 3:34 PMMarvin
11/30/2025, 3:34 PMMarvin
11/30/2025, 3:34 PMpoheng
11/30/2025, 3:51 PMMarvin
11/30/2025, 3:51 PMMarvin
11/30/2025, 3:51 PMname: my-project
deployments:
- name: hello-baked
entrypoint: /opt/prefect/flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
image: your-registry/my-project:1.2.3
image_pull_policy: "IfNotPresent" # or "Always"
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
If you want Prefect to build/push during deploy (optional):
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: your-registry/my-project
tag: 1.2.3
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
deployments:
- name: hello-baked
entrypoint: /opt/prefect/flows/flow.py:hello
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: "IfNotPresent"
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
Private registries
- Ensure the Docker host running the worker is authenticated:
- docker login your-registry
- Or provide registry credentials to Prefect via a DockerRegistryCredentials block and pass it in job_variables (or set it on the work pool’s base_job_template).
Key takeaway
- prefect.yaml pull = code retrieval steps at runtime, not Docker image pulls.
- Docker pulls the image automatically when the worker starts the job container; control this with image_pull_policy and by ensuring your image is built/pushed to a registry your worker can access.poheng
11/30/2025, 3:59 PMMarvin
11/30/2025, 3:59 PMMarvin
11/30/2025, 4:00 PMpoheng
12/01/2025, 12:47 AMMarvin
12/01/2025, 12:47 AMMarvin
12/01/2025, 12:48 AMpoheng
12/01/2025, 12:52 AMMarvin
12/01/2025, 12:52 AMMarvin
12/01/2025, 12:53 AMpoheng
12/01/2025, 1:10 AMMarvin
12/01/2025, 1:10 AMMarvin
12/01/2025, 1:11 AMFROM prefecthq/prefect:3-latest
# For stricter reproducibility, pin an exact tag, e.g.:
# FROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
# Install project dependencies defined in pyproject.toml into system site-packages
# (pip will read [project.dependencies] from pyproject.toml)
COPY pyproject.toml ./
RUN python -m pip install --upgrade pip && pip install .
# Copy your flow code into the image
# Ensure your flow entrypoint path matches this location
COPY flows ./flows
prefect.yaml (baked image; no pull section)
name: prefect_demo
prefect-version: 3.6.4
# Build and push the image during `prefect deploy` (optional if your CI builds/pushes)
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: your-dockerhub-user/prefect-demo
tag: v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No 'pull' section for baked-image method
deployments:
- name: my_baked_deployment
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: IfNotPresent
env:
# WSL2: point jobs at your local Prefect Server
PREFECT_API_URL: <http://host.docker.internal:4200/api>
parameters: {}
tags: [baked, docker]
# Optional schedule
# schedule:
# cron: "*/10 * * * *"
# timezone: Asia/Taipei
Notes
- Remove any git-related pull steps; code and deps are already in the image.
- Ensure your Docker worker is running and polling the docker-pool.
- If your Server and jobs share a named Docker network instead of host.docker.internal, set:
- job_variables.networks: ["prefect-network"]
- PREFECT_API_URL: http://prefect-server:4200/api
Deploy
- Build/push via prefect deploy -n my_baked_deployment
- Run from UI or:
prefect deployment run "prefect_demo/my_baked_deployment"
If you prefer uv, swap the install line in the Dockerfile:
RUN python -m pip install -U pip uv && uv pip install --system --frozen .
(Keep pyproject.toml in the image root; add uv.lock if you use one.)poheng
12/01/2025, 1:45 AMMarvin
12/01/2025, 1:45 AMMarvin
12/01/2025, 1:46 AMnetworks:
prefect:
name: prefect-network
driver: bridge
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports: ["4200:4200"]
networks: ["prefect"]
prefect-docker-worker:
image: prefecthq/prefect:3-latest
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
uv run --with 'prefect[docker]' prefect worker start --pool docker-pool
networks: ["prefect"]
prefect.yaml (baked image)
deployments:
- name: my_baked_deployment
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
job_variables:
image: your-registry/prefect-demo:latest
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Option B: talk to the host via the bridge gateway
- Publish the server port (4200) and use the Docker bridge gateway IP from inside containers, often 172.17.0.1:
PREFECT_API_URL=<http://172.17.0.1:4200/api>
- To be sure, find the gateway for your network:
- docker network inspect bridge | grep -i gateway
- docker network inspect prefect-network | grep -i gateway
Option C: make host.docker.internal work on Linux
- In docker-compose, add an extra host mapping:
extra_hosts:
- "host.docker.internal:host-gateway"
- For job containers (launched by the worker), add the same mapping via job_variables.container_create_kwargs:
job_variables:
image: your-registry/prefect-demo:latest
container_create_kwargs:
host_config:
ExtraHosts:
- "host.docker.internal:host-gateway"
env:
PREFECT_API_URL: <http://host.docker.internal:4200/api>
Pick one approach and make sure:
- The server listens on 0.0.0.0 and publishes port 4200
- The worker can reach the URL you set
- Your job containers inherit the correct network or extra_hosts so they can resolve and reach the API
Docs:
- Docker work pools: https://docs.prefect.io/v3/how-to-guides/deployment_infra/docker/
- Deployments with prefect.yaml: https://docs.prefect.io/v3/how-to-guides/deployments/create-deploymentspoheng
12/01/2025, 2:06 AMMarvin
12/01/2025, 2:06 AMMarvin
12/01/2025, 2:07 AMFROM prefecthq/prefect:3.6.4-python3.11 # pin to match your Python/lock; adjust as needed
WORKDIR /opt/prefect
# Copy dependency manifest(s) first for better caching
COPY pyproject.toml uv.lock ./
# Install into system site-packages (no venv needed at runtime)
RUN python -m pip install -U pip uv \
&& uv pip install --system --frozen .
# Copy your flow code into the image
COPY flows ./flows
docker-compose.yml (define a named network so jobs can reach the server)
version: "3.9"
networks:
prefect:
name: prefect-network
driver: bridge
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
networks: ["prefect"]
prefect-docker-worker:
image: prefecthq/prefect:3-latest
depends_on:
- prefect-server
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
uv run --with 'prefect[docker]'
prefect worker start
--pool docker-pool
networks: ["prefect"]
prefect.yaml (baked image; no pull)
name: my_prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked-image method
deployments:
- name: my_first_deployment
schedule:
cron: "*/2 * * * *"
timezone: "Asia/Taipei"
flow_name: main
# Absolute path inside the image is the most robust
entrypoint: /opt/prefect/flows/main.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: IfNotPresent
# Ensure the job container joins the same network as your server
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Notes and alternatives
- If you prefer not to add a named network, you can keep the job off that network and set PREFECT_API_URL to the bridge gateway, e.g., http://172.17.0.1:4200/api (check with docker network inspect bridge).
- If you really want to use a venv at build time (uv sync), then keep your original Dockerfile but set a job command that activates it:
- command: "sh -lc '. .venv/bin/activate && prefect flow-run execute'"
However, the system-site-packages approach above avoids this complexity.
Run order
- docker compose up -d
- prefect work-pool create docker-pool --type docker (once)
- prefect deploy -n my_first_deployment
- Trigger a run from the UI or:
- prefect deployment run "my_prefect_demo/my_first_deployment"
Quick verification
- From a throwaway container on the same network:
docker run --rm --network prefect-network alpine sh -lc "apk add --no-cache curl >/dev/null && curl -sSf <http://prefect-server:4200/api/health> && echo OK"
If you paste the exact folder name of your compose project, I can confirm the network name, or we can keep the explicit “prefect-network” as shown for stability.poheng
12/01/2025, 2:27 AMMarvin
12/01/2025, 2:27 AMMarvin
12/01/2025, 2:29 AMprefect deploy spun up a temporary local server and couldn’t find the work pool. Fix it by 1) pointing the CLI at your running server, 2) creating the work pool, then 3) redeploying.
Do this from your Linux host (not inside a container):
1) Point the CLI at your self-hosted API
- If your docker-compose exposes the server on port 4200, use localhost from the host:
export PREFECT_API_URL=<http://localhost:4200/api>
# or persist it to your Prefect profile:
prefect config set PREFECT_API_URL=<http://localhost:4200/api>
- Verify:
prefect config view | grep PREFECT_API_URL
2) Create the Docker work pool (once)
prefect work-pool create docker-pool --type docker
prefect work-pool ls
3) Redeploy using your existing prefect.yaml
prefect deploy -n my_first_deployment
4) Ensure your worker is running and polling docker-pool
- Your docker-compose worker should have PREFECT_API_URL=<http://prefect-server:4200/api> (inside Docker network) and start with prefect worker start --pool docker-pool.
- From the host, you can confirm:
prefect worker ls
Notes
- The “temporary server on http://127.0.0.1:8217” message appears when PREFECT_API_URL is not set; deployments created there will not appear in your real server.
- From the host, use <http://localhost:4200/api>. From job containers on the same Docker network, use <http://prefect-server:4200/api> and add job_variables.networks: ["prefect-network"] if you defined one.
Docs
- Work pools: https://docs.prefect.io/v3/concepts/work-pools
- Deployments: https://docs.prefect.io/v3/how-to-guides/deployments/create-deploymentspoheng
12/01/2025, 2:45 AMMarvin
12/01/2025, 2:45 AMMarvin
12/01/2025, 2:46 AMFROM prefecthq/prefect:3.6.4-python3.11 # pin to a known Python; adjust as needed
WORKDIR /opt/prefect
# Copy manifests first for better caching
COPY pyproject.toml uv.lock ./
# Install deps into system site-packages using uv (honors uv.lock)
RUN python -m pip install -U pip uv \
&& uv pip install --system --frozen .
# Copy your flow code
COPY flows ./flows
prefect.yaml (no pull section needed for baked image; use absolute entrypoint; remove HTML escapes)
name: my_prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
deployments:
- name: my_first_deployment
schedule:
cron: "*/2 * * * *"
timezone: "Asia/Taipei"
flow_name: main
entrypoint: /opt/prefect/flows/main.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: IfNotPresent
# If your server and jobs share a Docker network, include it here; otherwise omit
# networks:
# - prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Option 2: keep uv’s venv, but activate it at runtime
- If you want to keep “uv sync” (creates .venv), set the job command to activate it.
Dockerfile
FROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
RUN python -m pip install -U pip uv && uv sync --frozen
COPY flows ./flows
prefect.yaml (set command; also remove HTML escapes and unnecessary pull)
deployments:
- name: my_first_deployment
entrypoint: /opt/prefect/flows/main.py:main
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
command: "sh -lc '. .venv/bin/activate && prefect flow-run execute'"
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Additional cleanups
- Remove the pull: set_working_directory step; it’s not needed for baked images (and your absolute entrypoint ignores CWD anyway).
- Make sure your YAML doesn’t contain HTML-escaped characters:
- Use >= not >= in requires
- Use plain URLs, not <http://...>
- Ensure the job container can reach your server:
- If using a named Docker network, add job_variables.networks: ["prefect-network"] and keep PREFECT_API_URL=http://prefect-server:4200/api
- Otherwise, set PREFECT_API_URL to your host/ingress URL that the job can reach
If it still crashes, grab the job container logs to confirm the exact error:
- From the flow run page → Infrastructure tab → copy container ID, then:
- docker logs <container_id>
Post that snippet and I’ll pinpoint any remaining issue.poheng
12/01/2025, 3:03 AMMarvin
12/01/2025, 3:03 AMMarvin
12/01/2025, 3:04 AMfrom prefect import flow, task
import requests
import pandas as pd
import os
from io import BytesIO
from datetime import datetime
URL = "<https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet>"
@task(log_prints=True, retries=3, retry_delay_seconds=15)
def check_url_available(url: str) -> None:
print(f"Checking {url} status code")
resp = requests.head(url, timeout=15, allow_redirects=True)
resp.raise_for_status()
@task(log_prints=True)
def read_parquet_from_url(url: str) -> pd.DataFrame:
print(f"Downloading parquet from {url}")
resp = requests.get(url, timeout=60)
resp.raise_for_status()
buf = BytesIO(resp.content)
df = pd.read_parquet(buf) # requires pyarrow (or fastparquet) to be installed in the image
print(f"Loaded {len(df)} rows")
return df
@task(log_prints=True)
def write_data(df: pd.DataFrame, filepath: str) -> str:
os.makedirs(os.path.dirname(filepath), exist_ok=True)
df.to_csv(filepath, index=False) # fixed: use df.to_csv
print(f"Wrote {len(df)} rows to {filepath}")
return filepath
@flow(log_prints=True, name="main")
def main(
url: str = URL,
output_dir: str = "data",
filename_prefix: str = "yellow_tripdata",
) -> str:
ts = datetime.now().strftime("%Y-%m-%d_%H%M%S")
filepath = os.path.join(output_dir, f"{filename_prefix}_{ts}.csv")
check_url_available(url)
df = read_parquet_from_url(url)
return write_data(df, filepath)
if __name__ == "__main__":
# For baked-image deployments, prefer using `prefect deploy` with prefect.yaml.
# If you really want to deploy from code, provide pool/image explicitly:
# main().deploy(
# name="main",
# work_pool_name="docker-pool",
# job_variables={"image": "pohengchou/demo:test-v1"},
# )
main()
Key fixes and notes:
- Removed angle brackets around the URL and fixed the HTML escape for ->.
- Replaced the broken <http://df.to|df.to>_csv with df.to_csv.
- Added timeouts to requests.get and requests.head.
- pandas.read_parquet requires pyarrow (or fastparquet) to be in your baked image. Make sure your Dockerfile installs dependencies from pyproject.toml (e.g., via pip or uv) so pandas + pyarrow are present at runtime.
- For baked-image mode, do not use git pull steps. Use an absolute entrypoint in prefect.yaml that matches where you COPY your code (e.g., /opt/prefect/flows/main.py:main) and set job_variables.image to your baked image.
Quick sanity test locally before deploying:
- Run the script directly: python flows/main.py (or python -m flows.main), confirm it writes a CSV.
- Rebuild image with deps baked in, redeploy via prefect deploy -n my_first_deployment.
If you share your current Dockerfile and pyproject.toml, I can confirm the exact install line to ensure pandas/pyarrow/requests are baked correctly.poheng
12/01/2025, 3:07 AMMarvin
12/01/2025, 3:07 AMMarvin
12/01/2025, 3:08 AMFROM prefecthq/prefect:3.6.4-python3.11 # pin to match your deps/lock
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
# Install deps into system site-packages (no venv needed at runtime)
RUN python -m pip install -U pip uv \
&& uv pip install --system --frozen .
# Copy your flows
COPY flows ./flows
2) prefect.yaml (no pull, absolute entrypoint, fresh image, API reachable)
name: my_prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
deployments:
- name: my_first_deployment
entrypoint: /opt/prefect/flows/main.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always # avoids stale image during testing
networks:
- prefect-network # if your server & worker share this network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
schedule:
cron: "*/2 * * * *"
timezone: "Asia/Taipei"
3) Rebuild, push, redeploy
docker build -t pohengchou/demo:test-v1 .
docker push pohengchou/demo:test-v1
prefect deploy -n my_first_deployment
4) If it still crashes, grab the job container logs
- From the flow run in the UI → Infrastructure tab → copy container ID
- Or via labels:
docker ps -a --filter "label=<http://prefect.io/flow-run-id=019ad7d7-f393-7668-93be-32016d99ca1b|prefect.io/flow-run-id=019ad7d7-f393-7668-93be-32016d99ca1b>" --no-trunc
docker logs <container_id>
- You can also enable debug logging in the job:
job_variables:
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_LOGGING_LEVEL: DEBUG
Most common causes of exit 1 in baked-image
- Entrypoint path mismatch (e.g., using flows/main.py but you copied to a different location)
- Deps not actually installed into system site-packages (used uv sync without activating .venv)
- API not reachable from the job container (wrong URL or network not attached)
- Python/lock mismatch (e.g., lock for 3.11 but base image is 3.12) causing missing deps like pyarrow
Quick sanity test of the image
docker run --rm --network prefect-network pohengchou/demo:test-v1 \
sh -lc "python -c 'import pandas, pyarrow; import runpy; runpy.run_path(\"/opt/prefect/flows/main.py\")' && echo OK"
Post the container logs from the failed run and I’ll pinpoint the exact fix.Marvin
12/01/2025, 3:43 AMMarvin
12/01/2025, 3:43 AMpoheng
12/01/2025, 4:32 AMMarvin
12/01/2025, 4:33 AMMarvin
12/01/2025, 4:34 AMFROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
# Copy dependency manifests first for better layer caching
COPY pyproject.toml uv.lock ./
# Install deps into system site-packages (no venv needed at runtime)
# Uses the locked deps to ensure reproducible builds
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm -f requirements.txt
# Copy your flow code into the image
COPY flows ./flows
prefect.yaml (no pull section; deployment uses the baked image)
name: my_prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: your-dockerhub-user/prefect-demo
tag: v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked-image method
deployments:
- name: my_baked_deployment
# Use an absolute path that matches where you COPY your code in the Dockerfile
entrypoint: /opt/prefect/flows/main.py:main
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: IfNotPresent
# If your server and jobs share a named Docker network, include it (recommended on Linux)
# networks:
# - prefect-network
env:
# If using a named Docker network with your server container, use the service name:
# PREFECT_API_URL: <http://prefect-server:4200/api>
# Otherwise, if you expose port 4200 on the host, use the host URL:
PREFECT_API_URL: <http://localhost:4200/api>
Notes
- No pull steps: your code and dependencies are already in the image.
- If you’re running Prefect Server and the worker on a named Docker network (e.g., prefect-network), set job_variables.networks and point PREFECT_API_URL to http://prefect-server:4200/api.
- For testing to avoid stale images, you can temporarily set image_pull_policy: Always.
- Ensure your pyproject.toml lists pandas/pyarrow/requests/etc. If you don’t have a uv.lock yet, generate it locally with uv before building, or drop the “--locked” flag during early development.poheng
12/01/2025, 6:17 AMMarvin
12/01/2025, 6:17 AMMarvin
12/01/2025, 6:18 AMpull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.com/<group>/<repo>.git>
branch: main
credentials:
access_token: "{{ prefect.blocks.secret.gitlab-pat.value }}"
B) Use a GitLab credentials block (requires prefect-gitlab)
1) Install the integration for the environment running prefect deploy:
- uv run --with 'prefect[gitlab]' python -c "import prefect"
2) Create a GitLabCredentials block in the UI (Blocks → Create → GitLab Credentials) with your token.
3) Reference the block directly (no keypath) or its token field:
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.com/<group>/<repo>.git>
branch: main
credentials: "{{ prefect.blocks.gitlab-credentials.my-gitlab-creds }}"
# or
# credentials:
# access_token: "{{ prefect.blocks.gitlab-credentials.my-gitlab-creds.token }}"
C) If you must use SSH keys
- Put the private key into a Secret block (name e.g., gitlab-ssh-key) and reference .value:
- Use an SSH URL: git@gitlab.com:<group>/<repo>.git
- Then pass the key in the credentials per the step’s SSH parameters (varies by integration). If you want this path, tell me and I’ll give the exact snippet for SSH.
Also fix the worker/pool mismatch
- Your log shows ProcessWorker. If you intended Docker:
- Ensure the deployment’s work_pool.name is your Docker pool (e.g., docker-pool)
- Ensure a Docker worker is running for that pool:
- In compose or: uv run --with 'prefect[docker]' prefect worker start --pool docker-pool
- Verify: prefect worker ls
Quick checks
- List blocks to confirm names: prefect block ls
- Inspect your Secret block fields: prefect block inspect secret/gitlab-pat
- Redeploy after fixing prefect.yaml: prefect deploy -n <your-deployment-name>
If you paste the current pull section from your prefect.yaml and tell me which auth you prefer (HTTPS token vs SSH key), I’ll return the exact, corrected snippet.poheng
12/01/2025, 6:21 AMMarvin
12/01/2025, 6:21 AMMarvin
12/01/2025, 6:23 AMpull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.com/<group>/<repo>.git>
branch: main
access_token: "{{ prefect.blocks.secret.gitlab-pat }}"
B) HTTPS with a GitLabCredentials block (recommended if you use prefect-gitlab)
- Create the block (Blocks → Create → GitLab Credentials) named my-gitlab-creds (token + optional base_url for self-hosted).
- Reference the block as credentials (no field path):
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.com/<group>/<repo>.git>
branch: main
credentials: "{{ prefect.blocks.gitlab-credentials.my-gitlab-creds }}"
C) SSH (no parameters in git_clone)
- Use an SSH URL: git@gitlab.com:<group>/<repo>.git
- Ensure the job runtime (container or process host) already has:
- /home/<user>/.ssh/id_rsa (or another key) with 600 perms
- /home/<user>/.ssh/known_hosts containing gitlab.com
- Since git_clone has no known_hosts or ssh_key args, you must prepare SSH on the machine/image itself.
Action checklist
- Remove known_hosts (and any ssh_key) from your git_clone step — they’re not supported.
- Choose A/B/C above and update your prefect.yaml pull step accordingly.
- Ensure the deployment’s work_pool name is your Docker pool if you want Docker runs.
- Redeploy: prefect deploy -n <your-deployment-name>
If you paste your current pull section, I’ll rewrite it for your chosen auth (Secret block PAT vs GitLabCredentials vs SSH).poheng
12/01/2025, 6:26 AMMarvin
12/01/2025, 6:26 AMMarvin
12/01/2025, 6:27 AMname: prefect_demo
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.sltung.com.tw/t15989/docker_git.git>
branch: main
access_token: "{{ prefect.blocks.secret.gitlab-pat.value }}"
# If you need Python deps installed from the repo at runtime, add:
- prefect.deployments.steps.run_shell_script:
directory: "{{ clone.directory }}"
script: |
python -m pip install -U uv
uv sync
deployments:
- name: my_first_deployment
description: "我的第一個deployment"
schedule:
cron: "*/1 * * * *"
timezone: "Asia/Taipei"
flow_name: main
entrypoint: flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
# For Linux with a named network, prefer:
# networks: ["prefect-network"]
# env: { PREFECT_API_URL: "<http://prefect-server:4200/api>" }
env:
PREFECT_API_URL: "<http://localhost:4200/api>"
# Remove venv activation unless you actually created one; default command is fine:
# command: "prefect flow-run execute"
Option B: HTTPS with a GitLabCredentials block
- Create a GitLab Credentials block (gitlab-credentials/my-gitlab-creds) with your PAT and optional base_url.
- Requires installing the integration where you run prefect deploy (prefect[gitlab]).
pull:
- prefect.deployments.steps.git_clone:
id: clone
repository: <https://gitlab.sltung.com.tw/t15989/docker_git.git>
branch: main
credentials: "{{ prefect.blocks.gitlab-credentials.my-gitlab-creds }}"
Option C: SSH (no inline key or known_hosts params)
- Use an SSH URL:
- repository: git@gitlab.sltung.com.tw:t15989/docker_git.git
- Ensure the job image/container already has SSH configured:
- /home/prefect/.ssh/id_rsa (0600)
- /home/prefect/.ssh/known_hosts with gitlab.sltung.com.tw host key
- No ssh_private_key or known_hosts fields in git_clone. If you need help baking/mounting these, say so and I’ll provide a safe pattern.
Other fixes you should make now:
- Replace all HTML escapes in YAML:
- requires: prefect-docker>=0.3.1
- Remove <...> wrappers around URLs
- Replace && with && anywhere you use it
- The run was picked up by a ProcessWorker earlier; if you intend Docker:
- Ensure the deployment’s work_pool.name = your Docker pool (docker-pool)
- Ensure a Docker worker is running for that pool
- Verify with prefect worker ls
If you confirm which auth method you’re choosing (HTTPS token vs GitLabCredentials vs SSH), I’ll return the exact final prefect.yaml and, if needed, a Dockerfile snippet to preconfigure SSH for the job image.poheng
12/01/2025, 12:37 PMMarvin
12/01/2025, 12:37 PMMarvin
12/01/2025, 12:38 PMimage: '{{ build_image.image }}' — there is no image key in the build step output.
- Correct is image: '{{ build_image.image_name }}:{{ build_image.tag }}'
- Your build step uses dockerfile: auto
- That ignores your custom Dockerfile and auto-builds a generic image that copies your repo to /opt/prefect/<project-name>.
- If you want to use the Dockerfile you showed, set dockerfile: Dockerfile.
- You’re baking deps into a venv with uv sync, but not activating that venv at runtime
- PATH helps find executables but does not switch Python site-packages for the Prefect runner unless prefect runs from the venv. Safer: install into system site-packages, or explicitly activate the venv in the job command.
- You kept a pull step that only sets CWD
- For baked images, you don’t need any pull section. And if your job accidentally used the base Prefect image (because of the wrong image reference), that directory won’t exist anyway.
Two working fixes — pick one
Option A: Baked into system site-packages (simplest)
- Use your Dockerfile and install into system site-packages so no venv activation is needed.
Dockerfile
FROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm requirements.txt
COPY flows ./flows
prefect.yaml
name: prefect_docker
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo2
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked images
deployments:
- name: my-deployment
description: docker baked deployment
schedule:
cron: "* * * * *"
timezone: "Asia/Taipei"
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always
# If your server and jobs share a named network, include it and use the service URL:
# networks: ["prefect-network"]
# env: { PREFECT_API_URL: "<http://prefect-server:4200/api>" }
env:
PREFECT_API_URL: "<http://localhost:4200/api>"
Option B: Keep a venv, but activate it at runtime
- If you prefer uv sync venv, activate it in the job command.
Dockerfile
FROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
RUN python -m pip install -U pip uv && uv sync --frozen
COPY flows ./flows
prefect.yaml
name: prefect_docker
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo2
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
deployments:
- name: my-deployment
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always
command: "sh -lc '. /opt/prefect/.venv/bin/activate && prefect flow-run execute'"
env:
PREFECT_API_URL: "<http://localhost:4200/api>"
Why your container ls looked “correct”Marvin
12/01/2025, 12:38 PMdockerfile: auto copied your repo under /opt/prefect/prefect_docker, not because your custom Dockerfile ran.
- If you want your custom Dockerfile behavior, set dockerfile: Dockerfile and use absolute entrypoint /opt/prefect/flows/flow.py:main.
Next steps
- Update prefect.yaml with one of the options above (fix image reference; remove pull; set dockerfile to Dockerfile).
- Rebuild/push and redeploy:
- docker build -t pohengchou/demo2:test-v1 .
- docker push pohengchou/demo2:test-v1
- prefect deploy -n my-deployment
- If it still crashes, grab the job container logs:
- From the flow run → Infrastructure tab → container ID → docker logs <id>
- Or enable debug logging by adding:
- PREFECT_LOGGING_LEVEL: DEBUG in job_variables.env
This should resolve the exit code 1 you’re seeing.poheng
12/01/2025, 1:17 PMMarvin
12/01/2025, 1:17 PMMarvin
12/01/2025, 1:18 PMFROM prefecthq/prefect:3.6.4-python3.11 # pin to match your code/lock; avoid :latest for reproducibility
WORKDIR /opt/prefect
# Copy dependency manifests first for better layer caching
COPY pyproject.toml uv.lock ./
# Install deps into system site-packages (no venv to activate at runtime)
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm -f requirements.txt
# Copy your flow code into the image
COPY flows ./flows
prefect.yaml (baked image; no pull; absolute entrypoint; correct image reference)
name: prefect_docker
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo2
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked-image deployments
deployments:
- name: my-deployment
description: docker baked deployment
schedule:
cron: "* * * * *"
timezone: "Asia/Taipei"
# Use an absolute path that matches the Dockerfile COPY location
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
# IMPORTANT: reference name + tag from the build step
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
# During testing, force fresh pulls to avoid stale images
image_pull_policy: Always
# If your Prefect Server + worker run on a named Docker network (recommended on Linux),
# attach jobs to that network and use the service URL:
# networks:
# - prefect-network
# env:
# PREFECT_API_URL: <http://prefect-server:4200/api>
# If you're exposing 4200 on the host and not using a shared network, use the host URL:
env:
PREFECT_API_URL: <http://localhost:4200/api>
What changed vs your files
- Removed pull (no git_clone for baked images).
- Fixed the image reference to use image_name:tag from the build step.
- Switched to installing deps into system site-packages at build time (no need to activate a venv).
- Made entrypoint absolute: /opt/prefect/flows/flow.py:main.
- Replaced HTML escapes (>= and &&) with real characters.
- Added image_pull_policy: Always so you don’t hit stale images while testing.
Run it
- Ensure your CLI points at your server: prefect config set PREFECT_API_URL=<http://localhost:4200/api>
- Create the pool once: prefect work-pool create docker-pool --type docker
- Build/push/deploy:
- prefect deploy -n my-deployment
- Trigger a run from the UI or:
- prefect deployment run "prefect_docker/my-deployment"
If your server and worker are in docker-compose on a named network (e.g., prefect-network), uncomment networks and set PREFECT_API_URL to <http://prefect-server:4200/api>. If it still exits with code 1, grab the job container logs from the run’s Infrastructure tab and paste them here—I’ll pinpoint the exact error.poheng
12/01/2025, 1:25 PMMarvin
12/01/2025, 1:26 PMMarvin
12/01/2025, 1:26 PMversion: "3.9"
networks:
prefect:
name: prefect-network
driver: bridge
services:
postgres:
image: postgres:14
networks: [prefect]
...
redis:
image: redis:7
networks: [prefect]
...
prefect-server:
image: prefecthq/prefect:3-latest
networks: [prefect]
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
command: prefect server start --host 0.0.0.0
ports: ["4200:4200"]
...
prefect-docker-worker:
image: prefecthq/prefect:3-latest
networks: [prefect]
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
uv run --with 'prefect[docker]'
prefect worker start --pool docker-pool
prefect.yaml (for your baked image deployment):
deployments:
- name: my-deployment
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
job_variables:
image: your-registry/your-image:tag
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Option B: use the Compose default network
- If you don’t declare networks, Compose creates one named <project>_default (project = compose project name, typically the folder name).
- Find it and set that in job_variables.networks.
Find your project network name:
- It’s usually “<folder>_default”. Confirm with:
- docker network ls | grep _default
- or: docker compose ls (to see the project name), then use <project>_default
Then in prefect.yaml:
job_variables:
networks:
- <yourproject>_default
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Notes
- Do not use http://localhost:4200/api inside job containers; “localhost” is the container itself. Use the service name (prefect-server) on a shared network or attach to the host via other methods.
- If you stick with baked images, remove the pull section from prefect.yaml; it isn’t needed.
- During testing, consider image_pull_policy: Always to avoid stale images.
If you share the output of:
- docker compose ls (to confirm your project name)
- docker network ls | grep _default
I’ll tell you exactly what to put in job_variables.networks.poheng
12/01/2025, 1:40 PMMarvin
12/01/2025, 1:40 PMMarvin
12/01/2025, 1:41 PMFROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
# Copy dependency manifests first for caching
COPY pyproject.toml uv.lock ./
# Install deps into system site-packages (no venv needed at runtime)
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm -f requirements.txt
# Copy your flow code into the image
COPY flows ./flows
prefect.yaml (no pull, absolute entrypoint, correct image reference)
name: prefect_docker
prefect-version: 3.6.4
build:
- prefect_docker.deployments.steps.build_docker_image:
id: build_image
requires: prefect-docker>=0.3.1
image_name: pohengchou/demo2
tag: test-v1
dockerfile: Dockerfile
push:
- prefect_docker.deployments.steps.push_docker_image:
requires: prefect-docker>=0.3.1
image_name: '{{ build_image.image_name }}'
tag: '{{ build_image.tag }}'
# No pull section for baked-image deployments
deployments:
- name: my-deployment
description: docker baked deployment
schedule:
cron: "* * * * *"
timezone: "Asia/Taipei"
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
work_queue_name: default
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Checklist
- Ensure flows/flow.py exists in your repo and defines a function main.
- Make sure .dockerignore is not excluding flows/ or pyproject.toml/uv.lock.
- Your docker-compose must define the named network prefect-network and attach both prefect-server and the worker to it. The job container will join it via job_variables.networks.
Rebuild, deploy, test
- Rebuild and push (or let prefect deploy do it):
- docker build -t pohengchou/demo2:test-v1 .
- docker push pohengchou/demo2:test-v1
- Deploy:
- prefect config set PREFECT_API_URL=<http://localhost:4200/api>
- prefect work-pool create docker-pool --type docker (once)
- prefect deploy -n my-deployment
- Run a quick image sanity check:
docker run --rm --network prefect-network pohengchou/demo2:test-v1 \
sh -lc "python -c 'import pandas, pyarrow; import runpy; runpy.run_path(\"/opt/prefect/flows/flow.py\")' && echo OK"
If you still see a crash, grab the job container logs from the run’s Infrastructure tab and paste them here so I can pinpoint the exact error.poheng
12/01/2025, 1:44 PMMarvin
12/01/2025, 1:44 PMMarvin
12/01/2025, 1:44 PMdocker pull pohengchou/demo2:test-v1
docker run --rm -it pohengchou/demo2:test-v1 sh -lc "python -V; ls -la /opt/prefect; echo; ls -la /opt/prefect/flows || true"
- If you see flows/main.py, use /opt/prefect/flows/main.py:main
- If you see flows/flow.py, use /opt/prefect/flows/flow.py:main
- If flows/ is missing entirely, your Dockerfile didn’t COPY it (check .dockerignore and build logs)
Two working patterns (pick one)
Option A: Keep your current Dockerfile that copies to /opt/prefect/flows
- Dockerfile stays:
FROM prefecthq/prefect:3.6.4-python3.11
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm -f requirements.txt
COPY flows ./flows
- prefect.yaml must use the absolute path that matches the file name in flows:
deployments:
- name: my-deployment
entrypoint: /opt/prefect/flows/flow.py:main # or main.py:main if that’s your file
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always
networks: [prefect-network]
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
Option B: Keep your old auto layout (not recommended)
If you deliberately want /opt/prefect/prefect_docker/flows/, change the Dockerfile to copy there and set entrypoint to that path. But it’s simpler to use /opt/prefect/flows.
Common pitfalls to double-check
- Wrong filename in entrypoint: you used flows/flow.py:main but your file may be flows/main.py:main
- .dockerignore excludes flows/ so it never gets copied
- The build step in prefect.yaml uses the wrong Dockerfile (you set dockerfile: Dockerfile — good). Don’t use dockerfile: auto if you want your custom layout
- Stale image: use image_pull_policy: Always while testing
Finally, remove HTML escapes/angle brackets in YAML
- Change requires: prefect-docker>=0.3.1 to requires: prefect-docker>=0.3.1
- Change PREFECT_API_URL: <http://prefect-server:4200/api> to PREFECT_API_URL: <http://prefect-server:4200/api>
If the ls output from the docker run shows a different layout or missing flows, paste that output here and I’ll give you the exact entrypoint or Dockerfile COPY fix.poheng
12/01/2025, 2:01 PMMarvin
12/01/2025, 2:01 PMMarvin
12/01/2025, 2:02 PMdocker run check — that helps. Your image clearly has:
- Python 3.12.12
- A venv at /opt/prefect/.venv (created by uv sync)
- Code at /opt/prefect/flows/flow.py
Two things to fix:
1) Activate the venv at runtime (or install into system site-packages)
- Prefect does not auto-activate /opt/prefect/.venv. If your deps were installed into that venv, the flow will import with the system Python and crash (exit 1) when importing packages.
- Either activate the venv via job command, OR rebuild to install into system site-packages.
Option A: activate venv (fastest)
- In your prefect.yaml deployment, add a command and use real && (no HTML escape):
deployments:
- name: my-deployment
entrypoint: /opt/prefect/flows/flow.py:main
work_pool:
name: docker-pool
job_variables:
image: '{{ build_image.image_name }}:{{ build_image.tag }}'
image_pull_policy: Always
networks:
- prefect-network
env:
PREFECT_API_URL: <http://prefect-server:4200/api>
command: "sh -lc '. /opt/prefect/.venv/bin/activate && prefect flow-run execute'"
Option B: rebuild image with system installs (no venv)
- Update Dockerfile, rebuild, and you can omit the command:
FROM prefecthq/prefect:3.6.4-python3.11 # or pin a 3.12 tag and regenerate lock
WORKDIR /opt/prefect
COPY pyproject.toml uv.lock ./
RUN python -m pip install -U pip uv \
&& uv export --locked --no-dev -o requirements.txt \
&& pip install -r requirements.txt \
&& rm -f requirements.txt
COPY flows ./flows
2) Align Python version with your lock
- Your image is running Python 3.12.12. If your uv.lock was generated for 3.11, resolve for 3.12 or switch the base image to a 3.11 tag (e.g., prefect:3.6.4-python3.11). Mismatched locks can cause runtime import errors.
Also double-check
- Entrypoint path: you set /opt/prefect/flows/flow.py:main — that matches your image (good).
- Remove HTML escapes and angle brackets in YAML (>=, &&, plain URLs).
- Ensure the network exists: your compose should declare prefect-network and attach both prefect-server and the worker. The job joins it via job_variables.networks.
After changes
- prefect deploy -n my-deployment
- Trigger a run. If it still fails, grab the job container logs from the run’s Infrastructure tab or via:
- docker ps -a --filter "label=<http://prefect.io/flow-run-id=<flow-run-id|prefect.io/flow-run-id=<flow-run-id>>"
- docker logs <container_id>
If you prefer the system-install route, I can tweak your Dockerfile to pin specific versions and confirm the exact base tag for your lock.